Skip to content

💥 Add sandbox API client and split query params from request bodies - #21

Merged
marius-baseten merged 15 commits into
mainfrom
marius/sandbox-sdk
Oct 5, 2026
Merged

marius-baseten merged 15 commits into
mainfrom
marius/sandbox-sdk

Conversation

@marius-baseten

@marius-baseten marius-baseten commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

🚀 What

  • Adds a generated sandboxapi client from the Blaxel sandbox execution-plane spec, plus a thin SandboxClient/AsyncSandboxClient wrapper exported for direct execution-plane access; the hand-written baseten.sandbox surface is the higher-level face.
  • Updates the management spec from staging, adding the /v1/sandboxes CRUD and image paths plus /v1/token (prod spec does not serve these yet).
  • 💥 Breaking: query parameters now go on params; request bodies stay on request. Mirrors the JS change in baseten-js#22. Bumped to 0.12.0.

💻 How

  • Generator now emits success-code collections, non-JSON request bodies (multipart via httpx files, octet-stream via content), and a _raw sibling returning the unread httpx.Response for operations with non-JSON success content types (with an accept argument when the operation is content-negotiated).
  • Query and body may coexist only in the sandbox API; management and inference still enforce query-XOR-body and the generator raises if a spec breaks that.
  • Preprocess: injected query schemas renamed XRequest -> XParams, inline 2xx response schemas hoisted into components, dotted schema names folded to PascalCase (archive.Change -> ArchiveChange), YAML specs supported.

🔬 Testing

  • New tests/client/test_sandbox.py (13 cases): bearer token and opt-out, multipart upload with query+body, octet-stream body, raw sibling accept, status-narrowed export (ExportResult vs ExportProgress), typed and untyped errors, async variants.
  • poe lint, poe typecheck, poe test (62 passed) green; generation is idempotent so the CI regen-diff holds.

🤖 Generated with Claude Code

Note on diff size: the "files changed" stat is inflated by GitHub's diff renderer on the two large repetitive files (management.json, managementapi/_models.py) — it deletes and re-adds identical lines. The actual diff is +19.3k/−1.4k; git diff main...HEAD --shortstat shows it.

marius-baseten and others added 2 commits September 29, 2026 16:54
Staging spec (api.staging.baseten.co/v1/spec) adds the /v1/sandboxes
surface over prod. Prod spec still serves 160 paths without sandboxes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Query parameters now go on `params` and request bodies on `request`
(both may coexist on sandbox operations; management and inference still
enforce query-XOR-body). Success responses are a code collection,
non-JSON bodies (multipart, octet-stream) are supported, and operations
with non-JSON success responses get a `_raw` sibling that takes an
`accept` and returns the httpx response unread.

Generator changes: rename injected query schemas XRequest -> XParams,
hoist inline 2xx response schemas into components, fold non-identifier
schema names (archive.Change -> ArchiveChange) into PascalCase, accept
YAML specs, and add the sandbox spec from blaxel-ai/sandbox.

Management spec updated from staging, which adds /v1/sandboxes and
/v1/token over prod. New internal SandboxClient/AsyncSandboxClient
wrapper. Bumped to 0.12.0 for the breaking argument split.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 00:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Raw streaming responses are eagerly buffered, aliased request fields are unusable or serialized incorrectly, and mixed bodyless success statuses are mishandled.

Review effort: Balanced
Findings: 4 Medium severity

Open (4)
What changed in this PR

Adds generated sandbox API support and expands the generator for query/body separation, non-JSON payloads, and status-specific responses.

Changes:

  • Adds synchronous and asynchronous sandbox clients and generated models.
  • Separates query parameters into params and documents the breaking change.
  • Supports multiple success codes, raw responses, multipart bodies, and YAML specifications.
File Description
tests/​conftest.py Extends HTTP request/response fixtures.
tests/​client/​test_sandbox.py Tests sandbox client behavior.
tests/​client/​test_management.py Updates query-parameter usage.
scripts/​docgen/​conf.py Adds sandbox API documentation navigation.
scripts/​apigen/​preprocess.py Adds schema preprocessing and renaming.
scripts/​apigen/​clientgen.py Expands generated HTTP client behavior.
scripts/​apigen/​__main__.py Adds YAML and sandbox generation.
README.md Documents the 0.12 migration.
pyproject.toml Bumps the package to 0.12.0.
baseten/​client/​sandboxapi/​_models.py Adds generated sandbox models.
baseten/​client/​sandboxapi/​_client.py Adds the generated sandbox client.
baseten/​client/​sandboxapi/​__init__.py Exposes generated sandbox types.
baseten/​client/​managementapi/​__init__.py Updates generated management exports.
baseten/​client/​inferenceapi/​_client.py Adds raw response and multi-status support.
baseten/​client/​_sandbox.py Adds internal sandbox client wrappers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread baseten/client/sandboxapi/_models.py
Comment thread scripts/apigen/clientgen.py Outdated
Comment thread scripts/apigen/clientgen.py Outdated
Comment thread scripts/apigen/clientgen.py Outdated
Query parameters referenced via components/parameters, and parameters
inherited from the path item, were dropped silently. The sandbox
management operations take their team selector (and cursor/limit on
listings) that way, so they generated without any way to pass a team.

The query-XOR-body invariant is now path-scoped: operations under
/v1/sandboxes may carry query parameters alongside a request body, the
rest of the management and inference APIs may not.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Sep 30, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Raw responses are eagerly consumed, optional bodies are required, and aliased request fields cannot be reliably populated or serialized.

Review effort: Balanced
Findings: 5 Medium severity

Open (5)

Comment thread scripts/apigen/clientgen.py Outdated
…dispatch

- Generated classes with aliased fields get populate_by_name=True, and
  request models serialize by_alias, so a field renamed for Python
  (async_ for "async") is passable by its Python name and reaches the
  API under its own.
- Raw methods stream: build_request + send(stream=True) via a new
  _do_raw; error paths read the body first, and the docstring says the
  caller closes the response.
- An operation with several success codes and at least one JSON schema
  always dispatches by status; bodyless codes (e.g. 204 beside a JSON
  200) yield None.

Co-Authored-By: Claude Code <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Media-type generation drops supported inputs, may negotiate incorrect responses, and exposes an invalid async stream type.

Review effort: Balanced
Findings: None

Resolved since last review (5)
Previously missed (3)

In code that hasn't changed since last review

Medium severity JSON selection drops binary and multipart request encodings

scripts/​apigen/​clientgen.py:197

When a request body declares JSON plus other encodings, this unconditionally selects JSON and drops the others. The inference PredictInput declares application/json, application/octet-stream, and multipart/form-data, so every generated predict* method only accepts request: PredictInput; callers still cannot send binary or multipart inference inputs. Generate encoding-specific siblings (or an explicit content-type/body API) instead of discarding declared encodings.

Medium severity Async methods accept incompatible synchronous IO streams

scripts/​apigen/​clientgen.py:542

This same signature is emitted for async methods, but a regular IO[bytes] becomes a synchronous HTTPX request stream, which AsyncClient.send() rejects. The async post_process_stdin annotation therefore promises an input that fails at runtime. Emit bytes | str | AsyncIterable[bytes] for async clients (or restrict them to buffered values), while retaining IO[bytes] only for sync clients.

Medium severity Typed JSON sibling omits Accept for raw response operations

scripts/​apigen/​clientgen.py:558

The typed JSON sibling leaves Accept unset even when the operation also has raw response media types. HTTPX then sends its default Accept: */*; for example, /filesystem/{path} explicitly uses Accept to select JSON versus octet-stream, so get_filesystem() can receive binary data and fail in _do_json. Set Accept: application/json for the non-raw sibling whenever op.raw_accepts is non-empty.

marius-baseten and others added 2 commits September 29, 2026 18:05
Co-Authored-By: Claude Code <noreply@anthropic.com>
The thin per-sandbox wrapper is the advanced-user entry to the generated
execution-plane client, mirroring the JS SDK's @basetenlabs/client export.
The higher-level surface stays in baseten.sandbox.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@cretz cretz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, only minor things

Comment thread README.md
Comment thread pyproject.toml
Comment thread scripts/apigen/__main__.py Outdated
Comment thread scripts/apigen/specs/management.json
Comment thread scripts/apigen/__main__.py Outdated
Comment thread baseten/client/sandboxapi/_client.py
marius-baseten and others added 2 commits September 30, 2026 09:53
The staging pull had swapped every api.baseten.co URL in servers and
examples for the staging host; the snapshot is hand-curated from staging
until prod serves the sandbox paths, but its URLs should stay prod. The
spec download URL reverts to prod to match the JS SDK, and the README
keeps its shape as before this PR.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The generated client already handled both uniformly; the gate was only a
build-time spec lint, and it needed a /v1/sandboxes carve-out to accept
the current spec, so it guarded nothing. Server-side conventions like
query-XOR-body belong to the server's spec tests, not the SDK build.

Co-Authored-By: Claude Code <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/apigen/__main__.py Outdated
Comment thread README.md
Comment thread scripts/apigen/clientgen.py Outdated
yaml.safe_load yields datetime.date for date-only scalars, which the
unbound datetime.isoformat default rejects; serialize with the value's
own isoformat. Raw-sibling docstrings appended a sentence after summaries
that already end in a period, producing a doubled period.

Co-Authored-By: Claude Code <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Request media handling, content negotiation, async file input, and default streaming timeouts can produce runtime failures.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (3)
Previously missed (4)

In code that hasn't changed since last review

Medium severity Default sync client times out idle streaming responses

baseten/​client/​_sandbox.py:75

This default HTTPX client retains the 5-second read timeout, but the generated log and filesystem-watch methods return long-lived streams that may be idle for longer than five seconds. Out of the box, iterating either stream can therefore raise ReadTimeout during normal inactivity. Configure a stream-suitable read timeout or expose timeout configuration without requiring callers to replace the whole HTTP client.

Medium severity Default async client times out idle streaming responses

baseten/​client/​_sandbox.py:162

The async default client also keeps HTTPX's 5-second read timeout, so an idle get_process_logs_stream or get_watch_filesystem iterator fails with ReadTimeout even though these endpoints are intended to remain open. Configure a stream-suitable read timeout or expose timeout configuration without requiring a full client override.

Medium severity JSON sibling lacks Accept header for raw response alternatives

scripts/​apigen/​clientgen.py:557

The typed JSON sibling leaves Accept unset even when the operation can also return a raw representation. HTTPX then sends Accept: */*, so the server may legitimately choose application/octet-stream, after which _do_json raises despite a successful response. Set Accept: application/json for the JSON sibling whenever raw alternatives exist.

Medium severity Query-plus-body support violates management API invariant

scripts/​apigen/​preprocess.py:235

Removing this guard enables query-plus-body operations for every generated API, not only sandbox. The updated management spec already generates create_sandbox, update_sandbox, and push_image with both params and request, contradicting the PR's stated management query-XOR-body invariant. Either enforce this per API (with an explicit sandbox exception) or update the stated contract and cover management coexistence as intentional.

Comment on lines +194 to +197
content_types = list(rb.get("content", {}))
if _JSON_CONTENT in content_types:
return ""
return content_types[0] if content_types else ""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching the JS generator deliberately: it also prefers JSON when an operation declares several body encodings (the same choose-JSON rule in his clientgen), and the extra media types on PredictInput were equally unreachable before this PR. A content selector for multi-media bodies is a design question tracked with Chad alongside the accept/response-type design he offered to help with.

Comment thread scripts/apigen/clientgen.py Outdated
marius-baseten and others added 3 commits September 30, 2026 12:58
AsyncClient cannot send a sync file object, so the async octet-stream
body signature accepts bytes and str, while the sync one keeps IO[bytes].

Co-Authored-By: Claude Code <noreply@anthropic.com>
Forward-patches the spec snapshot with GET /v0/sandbox/hub and the
SandboxHubImage/SandboxHubImageList schemas, shaped from the live
Blaxel spec, and regenerates the typed client.

Co-Authored-By: Claude Code <noreply@anthropic.com>
SandboxHubImage -> SandboxLibraryImage and ListSandboxLibraryImages in
the spec snapshot, matching the library_models naming of the consumer
catalog. Type and operation names are client-side only; the wire path
stays /v0/sandbox/hub.

Co-Authored-By: Claude Code <noreply@anthropic.com>
marius-baseten and others added 3 commits October 2, 2026 10:03
Replaces the Blaxel hub forward-patch with GET /v1/sandboxes/library
_images from the served management spec (baseten#31334), matching
baseten-go 7b8c438. The server drops hidden and coming-soon entries.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Matches what baseten#31334 will serve, per baseten-go 7b8c438: state
and display_name drop from the sandbox record and create/update
requests, create_if_not_exists arrives on create.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@marius-baseten
marius-baseten merged commit cc581e1 into main Oct 5, 2026
5 checks passed
@marius-baseten
marius-baseten deleted the marius/sandbox-sdk branch October 5, 2026 17:44
@cretz cretz changed the title Add sandbox API client and split query params from request bodies 💥 Add sandbox API client and split query params from request bodies Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants